#!/usr/bin/env python3
#
# Videos accessible from terminal
# @utor: soneca
#
import os
import sys
import re
import argparse
import configparser
import requests
import logging
import json
import diskcache
from plyer import notification
from colorama import Fore, Back, Style

## Loggging setup
logging.basicConfig(
	level=logging.INFO,
	# datefmt='%H:%M:%S %d/%m/%Y',
	datefmt='%H:%M:%S',
	format='[%(levelname)s] [%(asctime)s] %(message)s',
	stream=sys.stdout
)

## Caching setup
CACHE_DIR = os.path.expanduser("~/.cache/invstream")
cache = diskcache.Cache(CACHE_DIR)
ttl = 86400 # Cache expiration time in seconds (defaults to 24 hours)

## User tor proxy (1=yes, 0=no)
tor_proxy = 1;

HD_QUALITY = "720p"
SD_QUALITY = "360p"
resolution = [HD_QUALITY,SD_QUALITY]
## Terminal callable player
player = "mpv"
## Useragent header for the requests
user_agent = {"User-agent":	"Mozilla/5.0 (Windows NT 10.0; rv:102.0) Gecko/20100101 Firefox/126.0"}
## Proxy SOCKS do Serviço Tor
tor_proxy_address = {
	"http": "socks5://127.0.0.1:9050",
	"https": "socks5://127.0.0.1:9050"
}
# Opções legíveis para o usuário
OPT_DIC = {
	"1": "habilitado",
	"0": "desabilitado",
	"18": "SD",
	"22": "HD"
}
default_instances = [
	"https://invidious.silkky.cloud",
	"https://inv.bp.projectsegfau.lt",
	"https://invidious.weblibre.org"
]
# Bad responses instances
bad_instances = [
	"https://yewtu.be",
	"https://invidious.protokolla.fi",
    "https://yt.oelrichsgarcia.de",
    "https://onion.tube",
]

# Arquivo de configuração
HOME = os.path.expanduser("~")
CFGFILE = f"{HOME}/.invstreamrc"



def error_hanler(e: str) -> None:
	"""
	Handle errors

	Args:
		e (str): Error message
	"""

	logging.error(e)
	notification.notify(
		title="Invstream",
		message=f"Erro encontrado: {e}",
		app_name="Invstream",
		app_icon="video-player",
		timeout=10
	)
	exit(1)


def load_configs() -> configparser.ConfigParser:
	"""
	Load configurations

	Returns:
		int: 0 if success, 1 if fail
	"""

	global tor_proxy
	global user_agent
	global resolution
	global player
	global default_instances
	global bad_instances
	global ttl

	if os.path.exists(CFGFILE):
		config = configparser.ConfigParser()
		try:
			config.read(CFGFILE)
		except:
			logging.warning(f"Falha ao carregar {CFGFILE}")
			return 0
		
		if "tor_proxy" in config['DEFAULT']:
			if config['DEFAULT']['tor_proxy'] == "yes":
				tor_proxy = 1
			elif config['DEFAULT']['tor_proxy'] == "no":
				tor_proxy = 0
			else:
				logging.warning(f"Tor proxy \"{config['DEFAULT']['tor_proxy']}\" inválido, use \"yes\" ou \"no\" (default: \"yes\")")

		if "user_agent" in config['DEFAULT']:
			user_agent = {"User-Agent": config['DEFAULT']['user_agent']}

		if "quality" in config['DEFAULT']:
			if config['DEFAULT']['quality'] == "sd":
				resolution = [SD_QUALITY,HD_QUALITY]
			elif config['DEFAULT']['quality'] == "hd":
				resolution = [HD_QUALITY,SD_QUALITY]
			else:
				logging.warning(f"Qualidade \"{config['DEFAULT']['quality']}\" inválida, use \"sd\" ou \"hd\" (default: \"hd\")")

		if "player" in config['DEFAULT']:
			player = config['DEFAULT']['player']

		if "default_instances" in config['instances']:
			default_instances = config['instances']['default_instances'].split('\n')

		if "bad_instances" in config['instances']:
			bad_instances = config['instances']['bad_instances'].split('\n')

		if "cache_timeout" in config['DEFAULT']:
			try:
				cache_timeout = config['DEFAULT']['cache_timeout']
				ttl = int(cache_timeout)
			except ValueError:
				logging.warning(f"Valor inválido para cache_timeout ({cache_timeout}), certifique-se de que é um número inteiro.")

		return config
	else:
		logging.warning(f"Customize your options in \"{CFGFILE}\"")
		return None


def get_instances() -> list:
	"""
	Get instances

	Returns:
		list: List of instances
	"""
	global user_agent
	global tor_proxy_address
	global default_instances

	# Get instances from cache if possible
	if cache.get("instances") is not None:
		logging.info("Instâncias carregadas do cache")
		return cache.get("instances")
	
	try:
		logging.info("Instâncias carregadas da api invidous.io")
		url = "https://api.invidious.io/instances.json?pretty=1&sort_by=health"
		with requests.get(url,proxies=tor_proxy_address, headers=user_agent, timeout=5) as response:
			instancias = response.json()

		saida = []
		for instancia in instancias:
			if instancia[1]["type"] == "https":
				saida.append(instancia[1]["uri"])

		for num in range(0, len(saida)):
			if(saida[num][-1] == '/'):
				saida[num] = saida[num][0:-1]

		# Save instances into cache
		try:
			cache.set("instances", saida, ttl)
			logging.info(f"Instâncias salvas no cache.")
		except Exception as e:
			logging.error(f"Falha ao salvar instâncias no cache: {e}")
		
	except KeyboardInterrupt:
		error_hanler("Interrupção: encerrando...")
	except Exception as e:
		saida = default_instances
		logging.warning(f"Error: {e}")
		logging.warning(f"Requisição por instâncias falhou, usando: {str(saida)}")

	logging.info(f"Instâncias: {len(saida)}")
	return saida;


def fmt_duration(nseg: int) -> str:
	"""
	Format duration showed in videos

	Args:
		nseg (int): Duration in seconds

	Returns:
			str: Formatted duration
	"""
	saida = ""
	if(nseg/3600 >= 1):
		saida += str(format(nseg//3600, '02d')) + ":"
		nseg = nseg%3600
	saida += str(format(nseg//60, '02d')) + ":"
	nseg = nseg%60
	saida += str(format(nseg, '02d'))
	return saida


def search_video(search_term: str, instancias: list) -> list:
	"""
	Search video

	Args:
		search_term (str): Search term
		instancias (list): List of instances
	
	Returns:
		list: List of videos
	"""

	# Tenta cada instância até ter sucesso
	for instancia in instancias:
		if instancia in bad_instances:
			continue

		url = f"{instancia}/api/v1/search?type=video&q={search_term}"
		try:
			logging.info(f"Requisitando: {url}")
			if(tor_proxy):
				r = requests.get(url, proxies=tor_proxy_address, headers=user_agent, timeout=3)
			else:
				r = requests.get(url, headers=user_agent, timeout=3)
			r.encoding = "unicode"
		except KeyboardInterrupt:
			error_hanler("Interrupção: encerrando...")
		except:
			logging.warning(f"requests: Falha de conexão")
			continue

		if r.status_code == 200:
			logging.info(f"{str(len(r.text))} bytes recebidos")
		else:
			logging.warning(f"HTTP Status Code: {r.status_code}")
			continue

		# Tenta receber dados da API
		try:
			dados = r.json()
		except:
			logging.warning("json: Problema com os dados")
			continue

		# Verifica presença de dados JSON
		if len(dados) == 0:
			logging.info("Nada encontrado")
		else:
			break

	if 'dados' in vars():
		return dados
	else:
		error_hanler("Desculpe, confira se você está conectado ou atualize as instâncias")


def play_media(vid, instancias, midia, player) -> int:
	"""
	Play media

	Args:
		vid (str): Video ID
		instancias (list): List of instances
		midia (str): Type of media
		player (str): Player to use

	Returns:
			int: 0 if success, 1 if fail
	"""
	try:
		## Prioriza instância introduzida pelo usuário
		instancia0 = re.search("https?://[^/]*", video_link).group(0)
		if instancias.count(instancia0):
			instancias.remove(instancia0)
		if ('youtu.be' not in instancia0) and ('youtube.com' not in instancia0):
			instancias.insert(0, instancia0)
	except:
		pass

	## Em cada instância até funcionar
	for instancia in instancias:
		if instancia in bad_instances:
			logging.info(f"Instance skipped: {instancia}")
			continue

		## Tenta conectar à instância
		url = f"{instancia}/api/v1/videos/{vid}"
		try:
			if(tor_proxy):
				logging.info(f"Requisitando: {url}")
				r = requests.get(url, proxies=tor_proxy_address, headers=user_agent, timeout=3)
			else:
				logging.info(f"Requisitando: {url}")
				r = requests.get(url, headers=user_agent, timeout=3)
			r.encoding = "unicode"
		except KeyboardInterrupt:
			error_hanler("Interrupção: encerrando...")
		except:
			logging.warning(f"Falha de conexão na instância: {instancia}")
			continue

		## Recebe os dados
		logging.info(f"{str(len(r.text))} bytes recebidos")
		try:
			dados = r.json()
		except:
			logging.warning(f"Erro nos dados recebidos")
			continue

		# ## Salva dados para debug
		# with open(f"/tmp/debug_{vid}.json", "w+") as f:
		# 	json.dump(dados, f, indent=4)

		if "type" in dados and dados["type"] == "livestream":
			logging.warning("Erro: livestreams não funcionam :(")
			return 1

		if("formatStreams" not in dados or len(dados["formatStreams"])==0):
			# Se reportar erro
			if("error" in dados and len(dados["error"])>0):
				# Imprime erro reportado
				logging.warning(f"Erro recebido: {dados['error']}")

				if(dados["error"]=="Sign in if you've been granted access to this video"):
					error_hanler("O vídeo está privado.")

				elif("Premieres " in dados["error"]):
					error_hanler("O vídeo ainda não foi publicado.")

			else:
				logging.warning("Link de fluxo não encontrado!")
			continue

		## Pediu somente áudio
		if midia == 'audio':
			for stream in dados["adaptiveFormats"]:
				if(stream["itag"] == "251" or stream["itag"] == "140"):
					vid_link = stream["url"]
					logging.info(f"URL: {vid_link}")
		## Pediu vídeo
		elif midia == 'video':
			for stream in dados["formatStreams"]:
				## Primeiro tenta a qualidade preferida
				if(stream["resolution"] == resolution[0]):
					vid_link = stream["url"]
					logging.info(f"URL: {vid_link}")
					logging.info(f"Qualidade: {Fore.CYAN}{stream['resolution']}{Style.RESET_ALL}")
			## Se não tiver a qualidade preferida
			if 'vid_link' not in vars():
				logging.info("Sem link para transmissão em HD")
				## Tenta a outra
				for stream in dados["formatStreams"]:
					if(stream["resolution"] == resolution[1]):
						vid_link = stream["url"]
						logging.info(f"URL: {vid_link}")
						logging.info(f"Qualidade: {Fore.YELLOW}{stream['resolution']}{Style.RESET_ALL}")

		## Se recebeu link
		if 'vid_link' in vars():
			logging.info(f"Chamando o player {player}")
			if player == "mpv":
				player = f'{player} --title={dados["title"].replace(" ", "_")}'
			## Verifica se o usuário definiu para usar Tor
			if tor_proxy:
				logging.info("Tor: YES")
				## Mensagem sobre o VLC não funcionar com Tor
				if player == "vlc":
					logging.info("VLC não funciona via tor")
					logging.info("Tente outro player ou desative o tor")
				
				try:
					os.execvp("proxychains", ["proxychains", "-q", *player.split(' '), vid_link])
				except Exception as e:
					handle_error(e)
			else:
				logging.info("Tor: NO")

				try:
					os.execvp(player.split(' ')[0], [*player.split(' '), vid_link])
					# os.startfile(vid_link)
				except Exception as e:
					handle_error(e)
			#break
			return 0
	error_hanler("Tentativa de reprodução falhou")


def parse_args() -> dict:
	"""
	Parse arguments

	Returns:
		dict: Arguments
	"""

	global tor_proxy
	global resolution

	example_text = """
	\rexample:

	\r	invstream --search \"search term\" video
	\r	invstream -is- audio
	"""

	parser = argparse.ArgumentParser(
			prog='InvStream',
			description='Broadcast videos from terminal',
			epilog = example_text,
			formatter_class=argparse.RawDescriptionHelpFormatter
		)
	parser.add_argument('mode', choices=['audio', 'video'], help='What do you want?')
	parser.add_argument('--tor', choices=['yes', 'no'], help='Whether or not to use tor proxy')
	parser.add_argument('-q', '--quality', choices=['hd', 'sd'], help='Video quality')
	parser.add_argument('-u', '--url', help="video URL")
	parser.add_argument('-i', '--interactive', action='store_true', help='Interactive Search')
	parser.add_argument('-s', '--search', help='Search terms (also use \"-i\" for interactive input)')
	args = parser.parse_args()

	if args.tor == "yes":
		tor_proxy = 1
	elif args.tor == "no":
		tor_proxy = 0

	if args.quality == "sd":
		resolution = [SD_QUALITY,HD_QUALITY]
	elif args.quality == "hd":
		resolution = [HD_QUALITY,SD_QUALITY]

	return args


def main() -> int:
	"""
	Main function

	Returns:
		int: 0 if success, 1 if fail
	"""
	global tor_proxy
	global player

	load_configs()
	args = parse_args()

	instancias = get_instances()
	if args.search:
		if args.search:
			if args.search == "-":
				search_terms = input(f"\t{Fore.GREEN}~~> Termos de pesquisa: {Style.RESET_ALL}")
			else:
				search_terms = args.search

		logging.info(f"Pesquisando: {Fore.YELLOW}{search_terms}{Style.RESET_ALL}")
		dados = search_video(search_terms.replace(" ", "+"), instancias)

		for video in dados:
			instancia = '/'.join(video['videoThumbnails'][0]['url'].split('/')[:3])
			print("-------------")
			if(video["type"] == "video"):
				print(f'{video["author"]} | {Fore.GREEN}{video["publishedText"]}{Style.RESET_ALL}')
				print(f'{Style.BRIGHT}{Fore.GREEN}{video["title"]}{Style.RESET_ALL} [{fmt_duration(video["lengthSeconds"])}] | {Fore.CYAN}{video["viewCountText"]}{Style.RESET_ALL}')
				print(f'{Fore.YELLOW}{instancia}/watch?v={video["videoId"]}{Style.RESET_ALL}')

				if args.interactive:
					opt = input("Assistir? [y/N] ")
					if opt == "sim" or opt == "s" or opt == "y" or opt == "yes":
						play_media(video["videoId"], instancias, args.mode, player)
	else:
		if args.url:
			video_link = args.url
		else:
			logging.info("URL format:\t /watch?v=(id do vídeo)")

			video_link = input(f"\t{Fore.GREEN}~~> Paste the video's URL: {Style.RESET_ALL}")

		## Captura ID de vídeo
		match = re.search(r"v=(.{11})|youtu\.be\/(.{11})", video_link)
		if match:
			if type(match.group(1)) == type(str()):
				vid = match.group(1)
			elif type(match.group(2)) == type(str()):
				vid = match.group(2)
			logging.info(f"ID do vídeo: {vid}")
		else:
			error_hanler("Url não compreendida")

		play_media(vid, instancias, args.mode, player)


if __name__ == "__main__":
	try:
		main()
	except KeyboardInterrupt:
		logging.warning("CTRL+C: Encerrando...")
